# SQLOperation.java

    package org.codefilarete.stalactite.sql.statement;

    import javax.annotation.Nullable;
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.SQLException;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;

    import org.codefilarete.stalactite.sql.ConnectionProvider;
    import org.codefilarete.stalactite.sql.statement.SQLStatement.BindingException;
    import org.codefilarete.tool.bean.Objects;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    /**
     * Tries to simplify usage of {@link PreparedStatement} in oriented scenario like:
     * - set values on {@link PreparedStatement}
     * - executeBatch {@link PreparedStatement} (see {@link WriteOperation})
     *
     * Logging of SQL execution can be activated with a logger with this class name.
     * If you want more fine-grained logs, SQL statements can be logged with DEBUG level, whereas values can be logged with TRACE level.
     * <b>Despite that activation of fined grained logs defers by level, they are always logged at DEBUG level.</b> (which is not really consistent).
     *
     * @see WriteOperation
     * @see ReadOperation
     * @param <ParamType> type of sqlStatement value entries, for example String (for {@link StringParamedSQL}), Integer (for {@link PreparedSQL}
     *
     * @author Guillaume Mary
     */
    public abstract class SQLOperation<ParamType> implements AutoCloseable {

    	/** Made public for internal project usage, not aimed at being used outside */
    	public static final Logger LOGGER = LoggerFactory.getLogger(SQLOperation.class);

    	/** Listener that does nothing, made to prevent from not-null-matching if */
    	public static final SQLOperationListener NOOP_LISTENER = new SQLOperationListener() {
    		/* Expected to do nothing, so we do nothing */
    	};

    	protected final ConnectionProvider connectionProvider;

    	protected PreparedStatement preparedStatement;

    	protected final SQLStatement<ParamType> sqlStatement;

    	private SQLOperationListener<ParamType> listener = NOOP_LISTENER;

    	private String sql;

    	/** Parameters that mustn't be logged for security reason for instance */
    	private Set<ParamType> notLoggedParams = Collections.emptySet();

    	/** Timeout for SQL orders, default is null meaning that JDBC default timeout applies, which is generally 0, which means no timeout */
    	private Integer timeout = null;

    	/**
    	 * Constructor with mandatory parameters
    	 * @param sqlStatement the statement that must be executed by this operation
    	 * @param connectionProvider JDBC {@link Connection} provider that will be used to get a connection to execute the statement on
    	 */
    	public SQLOperation(SQLStatement<ParamType> sqlStatement, ConnectionProvider connectionProvider) {
    		this.sqlStatement = sqlStatement;
    		this.connectionProvider = connectionProvider;
    	}

    	public ConnectionProvider getConnectionProvider() {
    		return connectionProvider;
    	}

    	public SQLStatement<ParamType> getSqlStatement() {
    		return sqlStatement;
    	}

    	public SQLOperationListener<ParamType> getListener() {
    		return listener;
    	}

    	public void setListener(@Nullable SQLOperationListener<ParamType> listener) {
    		this.listener = Objects.preventNull(listener, NOOP_LISTENER);
    	}

    	/**
    	 * Simple wrapping over {@link SQLStatement#setValues(Map)}
    	 * @param values values for each parameter
    	 */
    	public void setValues(Map<ParamType, ?> values) {
    		this.listener.onValuesSet(values);
    		// we transfer data to our own structure
    		this.sqlStatement.setValues(values);
    	}

    	/**
    	 * Simple wrapping over {@link SQLStatement#setValue(Object, Object)}
    	 *
    	 * @param index parameter/index for which value mumst be set
    	 * @param value parameter/index value
    	 */
    	public void setValue(ParamType index, Object value) {
    		this.listener.onValueSet(index, value);
    		this.sqlStatement.setValue(index, value);
    	}

    	/**
    	 * Common operation for subclasses. Rebuild PreparedStatement if connection has changed. Call {@link #getSQL()} when
    	 * necessary.
    	 */
    	protected void ensureStatement() {
    		try {
    			Connection connection = this.connectionProvider.giveConnection();
    			if (this.preparedStatement == null) {
    				prepareStatement(connection);
    			}
    		} catch (RuntimeException | SQLException e) {
    			throw new BindingException("Error while creating statement " + getSQL(), e);
    		}
    	}

    	/**
    	 * Expected to create the internal field {@link #preparedStatement} from given {@link Connection}
    	 * and {@link SQLStatement#getSQL()} result.
    	 * Exposed to eventually adapt the created {@link PreparedStatement}.
    	 *
    	 * @param connection the {@link Connection} to use to create the {@link PreparedStatement}}
    	 * @throws SQLException in case of error during {@link PreparedStatement} creation from the given {@link Connection}
    	 */
    	protected void prepareStatement(Connection connection) throws SQLException {
    		this.preparedStatement = connection.prepareStatement(getSQL());
    	}

    	/**
    	 * Gives the SQL that is used in the {@link PreparedStatement}.
    	 * Called by {@link SQLStatement#getSQL()} then stores the result.
    	 *
    	 * @return the SQL that is used in the {@link PreparedStatement}
    	 */
    	protected String getSQL() {
    		if (this.sql == null) {
    			this.sql = sqlStatement.getSQL();
    		}
    		return this.sql;
    	}

    	protected void prepareExecute() {
    		listener.onExecute(sqlStatement);
    		applyValuesToEnsuredStatement();
    		logExecution();
    		try {
    			applyTimeout();
    		} catch (SQLException e) {
    			throw new SQLExecutionException(getSQL(), e);
    		}
    	}

    	protected void applyValuesToEnsuredStatement() {
    		ensureStatement();
    		try {
    			this.sqlStatement.applyValues(preparedStatement);
    		} catch (RuntimeException e) {
    			throw new BindingException("Error while applying values " + this.sqlStatement.values + " on statement " + getSQL(), e);
    		}
    	}

    	/**
    	 * Cancels the underlying {@link PreparedStatement} (if exists and not closed, to avoid unnecessary exceptions)
    	 *
    	 * @throws SQLException this of the {@link PreparedStatement#cancel()} method
    	 */
    	public void cancel() throws SQLException {
    		if (this.preparedStatement != null) {
    			this.preparedStatement.cancel();
    		}
    	}

    	/**
    	 * Gives current {@link PreparedStatement}
    	 *
    	 * @return current {@link PreparedStatement}, maybe null, closed, cancel ...
    	 */
    	@Nullable
    	public PreparedStatement getPreparedStatement() {
    		return preparedStatement;
    	}

    	/**
    	 *
    	 * @return null means default timeout applies, else the timeout set, 0 means infinite (see JDBC specification)
    	 */
    	public Integer getTimeout() {
    		return timeout;
    	}

    	public void setTimeout(int timeout) {
    		this.timeout = timeout;
    	}

    	/**
    	 * Closes the internal {@link PreparedStatement}
    	 */
    	@Override
    	public void close() {
    		try {
    			if (preparedStatement != null) {
    				this.preparedStatement.close();
    			}
    		} catch (SQLException e) {
    			LOGGER.warn("Can't close statement properly", e);
    		}
    	}

    	/**
    	 * Set params that mustn't be logged when debug is activated for values
    	 * @param notLoggedParams set of not loggable values
    	 */
    	public void setNotLoggedParams(Set<ParamType> notLoggedParams) {
    		this.notLoggedParams = notLoggedParams;
    	}

    	protected Map<ParamType, Object> filterLoggable(Map<ParamType, ?> values) {
    		// we make a copy of values to prevent alteration
    		Map<ParamType, Object> loggedValues = new HashMap<>(values);
    		loggedValues.entrySet().forEach(e -> {
    			if (notLoggedParams.contains(e.getKey())) {
    				// we change logged value so param is still present in mapped params, showing that it hasn't disappeared
    				e.setValue("X-masked value-X");
    			}
    		});
    		return loggedValues;
    	}

    	protected void applyTimeout() throws SQLException {
    		if (getTimeout() != null) {
    			this.preparedStatement.setQueryTimeout(getTimeout());
    		}
    	}

    	protected void logExecution() {
    		logExecution(filterLoggable(sqlStatement.getValues()).toString());
    	}

    	protected void logExecution(String printableValues) {
    		// we log statement only in strict DEBUG mode because it's also logged in TRACE mode and DEBUG os also active at TRACE level
    		if (LOGGER.isDebugEnabled() && !LOGGER.isTraceEnabled()) {
    			LOGGER.debug(sqlStatement.getSQLSource());
    		} else if (LOGGER.isTraceEnabled()) {
    			LOGGER.trace("{} | {}", sqlStatement.getSQLSource(), printableValues);
    		}
    	}

    	/**
    	 * Contract to implement for being notified of actions on an {@link SQLOperation}
    	 * Created for use cases where listening to SQL orders is necessary but auditing logs is not enough because you only get a String version of
    	 * what is executed.
    	 * <strong>Be aware that sensible values are not filtered</strong> at the opposite to logged ones hence you get same values passed to SQL
    	 * order, DON'T LOG THEM. Hence this listener is not made to replace logging system.
    	 *
    	 * @param <ParamType> type of the {@link SQLOperation} to be registered on
    	 */
    	public interface SQLOperationListener<ParamType> {

    		/**
    		 * Called when the {@link SQLOperation#setValues(Map)} is called.
    		 * Please note that the given {@link Map} is writable which allow values to be modified, but this is not the primary goal on this method
    		 * and is not an active feature and may be changed in the future. This is done so because making it unmodifiable needs a superfluous
    		 * instantiation.
    		 * Please note also that this behavior defers from {@link #onValueSet(Object, Object)} where the value is "readonly" : since it is passed
    		 * by reference (Java language), simple (non complex) types are considered readonly.
    		 *
    		 * <strong>Be aware that sensible values are not filtered</strong> at the opposite to logged ones hence you get same values passed to SQL
    		 * order, DON'T LOG THEM.
    		 *
    		 * @param values
    		 */
    		default void onValuesSet(Map<ParamType, ?> values) {
    			// does nothing by default
    		}

    		/**
    		 * Called when the {@link SQLOperation#setValue(Object, Object)} is called.
    		 * <strong>Be aware that sensible values are not filtered</strong> at the opposite to logged ones hence you get same values passed to SQL
    		 * order, DON'T LOG THEM.
    		 *
    		 * @param param
    		 * @param value
    		 */
    		default void onValueSet(ParamType param, Object value) {
    			// does nothing by default
    		}

    		default void onExecute(SQLStatement<ParamType> sqlStatement) {

    		}
    	}
    }